package com.virjar.ratel.builder.ratelentry;

import com.google.common.collect.Lists;
import com.virjar.ratel.allcommon.NewConstants;
import com.virjar.ratel.builder.BootstrapCodeInjector;

import net.dongliu.apk.parser.bean.ApkMeta;

import org.apache.commons.io.FileUtils;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import org.jf.dexlib2.DexFileFactory;
import org.jf.dexlib2.Opcodes;
import org.jf.dexlib2.dexbacked.DexBackedDexFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static com.virjar.ratel.builder.ratelentry.Main.copyAndClose;

public class HelperCreateRDP {
    private final static String SMALI_DIRNAME = "smali";

    static void createRatelDecompileProject(BuilderContext context) throws IOException, InterruptedException {
        ApkMeta apkMeta = context.infectApk.apkMeta;
        File outputApkFile = context.outFile;
        Properties ratelBuildProperties = context.ratelBuildProperties;
        //1. first calculate an output project directory
        File outProject;
        if (context.cmd.hasOption('o')) {
            outProject = new File(context.cmd.getOptionValue('o'));
        } else {
            outProject = new File(
                    apkMeta.getPackageName() + "_" + apkMeta.getVersionName() + "_" + apkMeta.getVersionCode() + "_ratel_decompile");
        }

        System.out.println("output project is:" + outProject.getAbsolutePath());

        FileUtils.deleteDirectory(outProject);
        outProject.mkdirs();

        File rawResource = new File(outProject, "raws");

        List<Callable<String>> callableList = Lists.newLinkedList();

        //2. then decompile all *.dex file like apktool
        try (ZipFile zipFile = new ZipFile(outputApkFile)) {
            Enumeration<ZipEntry> entries = zipFile.getEntries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                String entryName = zipEntry.getName();
                if (entryName.endsWith(".dex") && entryName.startsWith("classes")) {
                    //decompile dex file,and skip asset dex files,this is a bad case to decompile assets/*.dex for apktool
                    File smaliDir;
                    if (entryName.equalsIgnoreCase("classes.dex")) {
                        smaliDir = new File(outProject, SMALI_DIRNAME);
                    } else {
                        smaliDir = new File(outProject, SMALI_DIRNAME + "_" + entryName.substring(0, entryName.indexOf(".")));
                    }
                    FileUtils.deleteDirectory(smaliDir);
                    smaliDir.mkdirs();

                    DexBackedDexFile dexFile = DexFileFactory.loadDexEntry(outputApkFile,
                            entryName, true, Opcodes.getDefault()
                    ).getDexFile();
                    callableList.add(new SmaliDecoder(dexFile, smaliDir, entryName));
                    //BootstrapCodeInjector.baksmali(null, dexFile, smaliDir);
                } else if (zipEntry.isDirectory()) {
                    FileUtils.forceMkdir(new File(rawResource, entryName));
                } else {
                    File destResourceFile = new File(rawResource, entryName);
                    FileUtils.forceMkdirParent(destResourceFile);
                    copyAndClose(zipFile.getInputStream(zipEntry), new FileOutputStream(destResourceFile));
                }
            }
        }

        ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        threadPool.invokeAll(callableList);
        threadPool.shutdown();

        //3. add rebuild script int the decompile project,ratel decompile project can be migrate to other computer event through there is no ratel environment
        File ratelConfigDir = new File(outProject, "ratel_resource");
        ratelConfigDir.mkdirs();

        ratelBuildProperties.setProperty("app.packageName", apkMeta.getPackageName());
        ratelBuildProperties.setProperty("app.versionName", apkMeta.getVersionName());
        ratelBuildProperties.setProperty("app.versionCode", String.valueOf(apkMeta.getVersionCode()));
        ratelBuildProperties.setProperty("app.appName", apkMeta.getName());

        ratelBuildProperties.store(new FileOutputStream(new File(ratelConfigDir, "ratelConfig.properties")), "auto generated by virjar@ratel");


        String baseName = NewConstants.BUILDER_RESOURCE_LAYOUT.RDP_BASE.getNAME();
        for (NewConstants.BUILDER_RESOURCE_LAYOUT layout : NewConstants.BUILDER_RESOURCE_LAYOUT.values()) {
            if (!layout.getNAME().startsWith(baseName)) {
                continue;
            }
            if (layout.isOnlyDev()) {
                continue;
            }
            String name = layout.getNAME().substring(baseName.length());
            // 特殊处理jar包格式
            if (name.endsWith(".jar.bin")) {
                name = name.substring(0, name.length() - 4);
            }
            File outFile = new File(outProject, name);
            FileUtils.copyFile(BindingResourceManager.get(layout), outFile);
            if (name.endsWith(".sh")) {
                outFile.setExecutable(true);
            }
        }
    }

    private static class SmaliDecoder implements Callable<String> {
        private final DexBackedDexFile dexFile;
        private final File smaliDir;
        private final String entryName;

        public SmaliDecoder(DexBackedDexFile dexFile, File smaliDir, String entryName) {
            this.dexFile = dexFile;
            this.smaliDir = smaliDir;
            this.entryName = entryName;
        }

        @Override
        public String call() {
            System.out.println("Baksmaling " + entryName + "...");
            BootstrapCodeInjector.baksmali(null, dexFile, smaliDir);
            System.out.println("Baksmaling " + entryName + " finish");
            return "";
        }
    }
}
